package ai.genetic;

import shared.MathHelper;

import java.text.DecimalFormat;
import java.util.*;

public class Evolver<O extends Object, E extends Environment<O, Individual<O>>>
{

    private int maxGenerations;

    private boolean elitism = true;

    private double selectionScalar = 0.9;

    private double mortalityRate = 0.7;

    private int branchingFactor = 1;

    private double breederPercent = 0.3;

    private E environment;

    private boolean printProgress = false;

    private double printFrequency = 10;

    private Random rand = new Random();

    private Collection<Individual<O>> population;

    public Evolver(E environment, Collection<Individual<O>> population)
    {
        this.environment = environment;
        this.population = population;
    }

    public Evolver()
    {
    }

    /**
     * evolves the population until the best individual has reached the environment's threshold
     * or max generation shave been reached
     *
     * @return the evolved group
     */
    public List<O> evolveGroup()
    {
        return evolveGroup(0);
    }

    /**
     * evolve group of people untill the nth best indivudual has resached the environments threshold
     * or max generation shave been reached
     *
     * @param n index of individual to check against threshold in best first ordering of population
     * @return entire evolved population
     */
    public List<O> evolveGroup(int n)
    {
        branchingFactor = (int) Math.round(2 / breederPercent);
        int breederSize = (int) Math.round(breederPercent * population.size());
        int childCount = breederSize % 2 == 0 ? (breederSize / 2) * branchingFactor : ((breederSize - 1) / 2) * branchingFactor;

        double actualMR = MathHelper.clamp(mortalityRate, 0, ((double) childCount) / population.size());

        double survivalRate = 1 - actualMR;
        double childSurvivalRate = (actualMR * population.size()) / childCount;

        Individual<O> threshIndividual = getNthBest(n);
        double threshFitness = environment.getFitness(threshIndividual);
        int generation = 0;
        long startTime = System.currentTimeMillis();
        while (threshFitness < environment.getFitnessThreshold() && generation < maxGenerations)
        {
            generation++;
            Collection<Individual<O>> breeders = select(population, breederSize);
            Collection<Individual<O>> children = crossover(breeders);

            for (Individual<O> individual : children)
            {
                individual.mutate();
            }

            //child properties finalized, update fitness values
            for (Individual<O> child : children)
            {
                environment.evaluateFitness(child);
            }


            population = select(population, (int) Math.round(survivalRate * population.size()));

            population.addAll(select(children, (int) Math.round(childSurvivalRate * children.size())));


            threshIndividual = getNthBest(n);
            threshFitness = environment.getFitness(threshIndividual);


            Individual<O>[] bw = getBestWorst();
            double bestFitness = environment.getFitness(bw[0]);
            double worstFitness = environment.getFitness(bw[1]);
            if (printProgress && generation % printFrequency == 0)
                System.out.println("Time: " + (System.currentTimeMillis() - startTime) + " Generation " + generation + " Population size: " + population.size() +
                                           " Best/Worst fitness: [ " + numFormat(bestFitness) + " " + numFormat(worstFitness) + " ]");


        }

        ArrayList<O> trainedObjects = new ArrayList<O>(population.size());

        for (Individual<O> p : population)
        {
            trainedObjects.add(p.getObject());
        }

        return trainedObjects;

    }

    public O evolveBest()
    {
        evolveGroup();

        return getBest().getObject();

    }

    private String numFormat(double d)
    {
        DecimalFormat format = new DecimalFormat("#.###");
        if (d != 0 && (Math.abs(d) > 1000 || Math.abs(d) < 0.001))
        {
            format = new DecimalFormat("#.###E00");
        }
        return format.format(d);
    }


    private Individual<O> getNthBest(int n)
    {
        List<Individual<O>> pop = new ArrayList<Individual<O>>(population);
        Collections.sort(pop, environment);
        return pop.get(n);
    }

    private Individual<O> getBest()
    {
        return getBestWorst()[0];
    }

    private Individual<O>[] getBestWorst()
    {
        Individual<O> best = null;
        Individual<O> worst = null;
        double maxFitness = Double.NEGATIVE_INFINITY;
        double minFitness = Double.POSITIVE_INFINITY;

        for (Individual<O> person : population)
        {
            double fitness = environment.getFitness(person);
            if (fitness > maxFitness)
            {
                maxFitness = fitness;
                best = person;
            }
            if (fitness < minFitness)
            {
                minFitness = fitness;
                worst = person;
            }
        }

        return new Individual[]{best, worst};

    }


    private Collection<Individual<O>> crossover(Collection<Individual<O>> breeders)
    {

        Collection<Individual<O>> children = new ArrayList<Individual<O>>();
        LinkedList<Individual<O>> remainingBreeders = new LinkedList<Individual<O>>(breeders);

        while (remainingBreeders.size() > 1)
        {
            Individual<O> parentA = remainingBreeders.remove(rand.nextInt(remainingBreeders.size()));
            Individual<O> parentB = remainingBreeders.remove(rand.nextInt(remainingBreeders.size()));

            for (int i = 0; i < branchingFactor; i++) children.add(parentA.crossover(parentB));
        }

        return children;
    }


    private Collection<Individual<O>> select(Collection<Individual<O>> population, int subsetSize)
    {
        Collection<Individual<O>> subset = new ArrayList<Individual<O>>();
        List<Individual<O>> sortedPopulation = new ArrayList<Individual<O>>(population);
        Collections.sort(sortedPopulation, environment);
        int selected = 0;

        if (elitism)
        {
            subset.add(sortedPopulation.get(0));
            sortedPopulation.remove(0);
            selected++;
        }

        while (selected < subsetSize)
        {

            int size = sortedPopulation.size();
            double pv[] = new double[size];
            pv[0] = (selectionScalar - 1) / (Math.pow(selectionScalar, size) - 1);
            for (int i = 1; i < size; i++)
            {
                pv[i] = selectionScalar * pv[i - 1];
            }


            int index = MathHelper.selectPVIndex(pv);

            subset.add(sortedPopulation.get(index));
            sortedPopulation.remove(index);
            selected++;

        }

        return subset;
    }

    private boolean percentChance(double p)
    {
        return rand.nextDouble() <= p;
    }


    public int getMaxGenerations()
    {
        return maxGenerations;
    }

    public void setMaxGenerations(int maxGenerations)
    {
        this.maxGenerations = maxGenerations;
    }


    public void setBreederPercent(double breederPercent)
    {
        this.breederPercent = breederPercent;
    }

    public E getEnvironment()
    {
        return environment;
    }

    public void setEnvironment(E environment)
    {
        this.environment = environment;
    }

    public Collection<Individual<O>> getPopulation()
    {
        return population;
    }

    public void setElitism(boolean elitism)
    {
        this.elitism = elitism;
    }

    public void setPrintProgress(boolean printProgress)
    {
        this.printProgress = printProgress;
    }

    public void setSelectionScalar(double selectionScalar)
    {
        this.selectionScalar = selectionScalar;
    }

    public void setPrintFrequency(double printFrequency)
    {
        this.printFrequency = printFrequency;
    }

    public void setPopulation(Collection<? extends Individual<O>> population)
    {

        this.population = new ArrayList<Individual<O>>(population);

        //new population compute fitness.
        for (Individual<O> person : population)
        {
            environment.evaluateFitness(person);
        }
    }

    public void setMortalityRate(double mortalityRate)
    {
        this.mortalityRate = mortalityRate;
    }
}
